package com.pc.cloud.core;

import com.pc.cloud.services.*;
import com.pc.cloud.utils.DocSet;
import com.pc.cloud.utils.DocSetCollector;
import com.pc.cloud.utils.NamePair;
import com.pc.cloud.utils.QueryUtils;
import org.apache.log4j.Logger;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.document.MapFieldSelector;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.*;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LuceneSearcher implements NewDocumentAdded {
	private final static Logger logger = Logger.getLogger(LuceneSearcher.class);
	private volatile IndexSearcher searcher;
	private LuceneSettings settings;
	private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); 
	private FieldSelector fieldSelector = new IDFieldSelector();
	private FacetSearch facetSearch = new FacetSearch();
	
    public IndexReader getReader() throws Exception {
        return reader;
    }

    public void setReader(IndexReader reader) {
        this.reader = reader;
    }

    public IndexSearcher getSearcher() throws Exception{
        return searcher;
    }

    public void setSearcher(IndexSearcher searcher) {
        this.searcher = searcher;
    }

    private IndexReader reader;
	
	
	public LuceneSearcher(LuceneSettings settings) throws Exception {
		super();
		this.settings = settings;
		this.openSearch();
	}

	public void newDocumentAdded(IndexReader r) throws Exception {
		rwlock.writeLock().lock();
			try {
				if(searcher != null) {
					this.searcher.close();
				}
				if(reader != null) {
					this.reader.close();
				}
				
				this.reader = r;
				this.searcher = new IndexSearcher(reader);
				logger.info("new DocumentAdded : " + this.reader.maxDoc());			
			}finally {
				rwlock.writeLock().unlock();
			}	
	}

	private void openSearch() throws Exception {
		try {			
			this.reader = IndexReader.open(this.settings.directory(), true);
			this.searcher = new IndexSearcher(reader);
		}
		catch (Exception e) {
			e.printStackTrace();
			throw e;
		}
	}

	public SearchResults search(SearchArgs args) throws Exception {
		long a = System.currentTimeMillis();		
		Query query = args.getQueryBuilder().buildQuery(args);
		Query filterQuery = args.getQueryBuilder().buildFilter(args);
        Filter filter = null;
        if(filterQuery != null) {
			filter = new QueryWrapperFilter(filterQuery);
		}
		Sort sort = args.getQueryBuilder().buildSort(args);
		List<String> facetFields = args.getFacetFields();
		
		//summary
		List<MetricArg> metricFields = args.getMetricFields();
		List<FacetQueryArg> facetQueryFields = args.getFacetQueryFields();
		
		String type = args.getSearchType().toString();
		logger.debug("searchType: " + type);
		logger.debug(query);
		logger.debug(filter);
		logger.debug(sort);
		if(facetFields != null) {
			for(String f : facetFields) {
				logger.debug("facetField : " + f);
			}
		}
		SearchResults sr = new SearchResults();
		rwlock.readLock().lock();
		try {
			TopDocs td;
			if(sort != null) {
				td = searcher.search(query, filter, args.getInitialRecord()+args.getFetchCount(),sort);
			} else {
				td = searcher.search(query, filter, args.getInitialRecord()+args.getFetchCount());
			}
			
		    ScoreDoc[] hits = td.scoreDocs;
		    int total = td.totalHits;		    
		    sr.setTotal(total);
		    
		    FieldSelector fselector = null;
		    String valueField = args.getValueField();
		    boolean fetchValues = false;
		    if( valueField != null && !"".equals(valueField)) {
		    	String[] fields = new String[]{"id",valueField};
		    	fselector = new MapFieldSelector(fields);
		    	fetchValues = true;
		    } else {
		    	fselector = this.fieldSelector;
		    }
		    
		    for (int i=args.getInitialRecord(); i<hits.length;i++) {
		    	Document hitDoc = searcher.doc(hits[i].doc,fselector);		      
		    	sr.getLists().add(hitDoc.get("id"));		    	
		    	
		    	if(fetchValues){
		    		sr.getValueLists().add(hitDoc.get(valueField));
		    	}
		    } 
		    
		    DocSet docs = null;
		    //facet search
		    if(facetFields != null && facetFields.size() >0) { 
		    	docs = getDocs(query,filter);
		    	for(String f : facetFields) {
		    		List<NamePair> pairs = facetSearch.getFieldCacheCounts(this,docs, f);
		    		sr.getFacets().put(f, pairs);
		    	}
		    }	
		    
		    //summary
		    if(metricFields != null && metricFields.size() > 0) {
		    	if(docs == null) {
		    		docs = this.getDocs(query, filter);
		    	} 		    	
		    	Collection<NamePair> sum = facetSearch.getSum(this,docs, metricFields);
		    	sr.setSum(sum);
		    }
		    if(facetQueryFields != null && facetQueryFields.size() > 0) {
		    	if(docs == null) {
		    		docs = this.getDocs(query, filter);
		    	} 
		    	
		    	Map<String,Collection<FacetSearchRow>> sumFacets = new HashMap<String,Collection<FacetSearchRow>>();
		    	for(FacetQueryArg fs : facetQueryFields){
		    		Collection<FacetSearchRow> facetSearch = this.facetSearch.getFacetSum(this, docs, fs);
		    		sumFacets.put(fs.getFacetField(), facetSearch);
		    	}
		    	sr.setSumFacets(sumFacets);
		    }
		    
		    
		} finally{
			rwlock.readLock().unlock();
		}	
		
long b = System.currentTimeMillis();
logger.debug("spend : " + (b -a ) + "ms");
		return sr;
	}
	
	private DocSet getDocs(Query query, Filter filter) throws Exception {
		DocSet docs = null;
		DocSetCollector collector = new DocSetCollector(this.reader.maxDoc()>>6,this.reader.maxDoc());
    	if(filter == null) {
    		this.searcher.search(query, collector);
    	} else {
    		this.searcher.search(query,filter, collector);
    	}
    	docs = collector.getDocSet();
		return docs;
	}
	
	//Only positive query
	public int numDocs(Query a, DocSet b) throws Exception {
	    Query absQ = QueryUtils.getAbs(a);
	    DocSet positiveA = getPositiveDocSet(absQ);
	    return a==absQ ? b.intersectionSize(positiveA) : b.andNotSize(positiveA);
	}
	
	// This method only supports TermQuery. 
	private DocSet getPositiveDocSet(Query absQ) throws Exception {
		//TODO Does it need a filterCache?
		
		DocSetCollector collector = new DocSetCollector(maxDoc()>>6, maxDoc());
		if (absQ instanceof TermQuery) {
	        Term t = ((TermQuery)absQ).getTerm();
	        int[] arr = new int[256];
	        int[] freq = new int[256];
	        TermDocs tdocs = this.searcher.getIndexReader().termDocs(t);
	        if(tdocs != null) {
	        	for(;;) {
		            int num = tdocs.read(arr, freq);
		            if (num==0) break;
		            for (int j=0; j<num; j++) {
		              collector.collect(arr[j]);
		            }
		        }
	        }	         
	        tdocs.close();	        
		} else {
			throw new Exception("unsupported Querys : " + absQ);
		}
		return collector.getDocSet();
	}

	private int maxDoc() throws IOException {		
		return this.searcher.maxDoc();
	}

	public void close() throws IOException {
		if (searcher != null) {
			searcher.close();
		}
	}
}
